﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SafeScript.MetaModel.Statements
{
    public class FunctionCallOperator : PropertyTailOperator
    {
        public List<Operator> Parameters = new List<Operator>();
        /// <summary>
        /// May be null. It contains the type of the function that has been called.
        /// </summary>
        public FunctionTypeDef FunctionType;
        /// <summary>
        /// May be null. It contains the event that is emitted.
        /// </summary>
        public MemberEvent Event;

        public override int GetPrecedence()
        {
            return 20;
        }

        public override ITypeDef ComputeType(ITypeDef goal, bool needsRef, ErrorReport report)
        {
            if (needsRef)
            {
                this.ReportNotAReferenceType(report);
                return null;
            }
            
            // Determine which function is to be invoked
            ITypeDef type = null;

            // Is this the invocation of a constructor?
            if (this.Operator is NewOperator)
            {
                // Get the return type
                InstanceType it = this.Operator.ComputeType(null, false, report) as InstanceType;
                // Some error?
                if ( it == null )
                    return null;
                Class newtype = it.Type as Class;
                if (newtype == null)
                    return null;
                // Get the constructor
                object con = newtype.GetConstructor();
                if (con == null)
                    return null;
                // Get the type
                if (con is MemberFunction)
                    type = (con as MemberFunction);
                else if (con is MemberFunctionSet)
                    type = (con as MemberFunctionSet).Type;
                if (type == null)
                    return null;
            }
            else
            {
                type = this.Operator.ComputeType(null, false, report);
                // Some error?
                if (type == null)
                    return null;
            }

            // Emitting an event?
            if (type is MemberEvent)
            {
                this.Event = type as MemberEvent;

                // Check that the call has exactly one argument
                if (this.Parameters.Count != 1)
                {
                    report.Add(new ErrorEntry(this.Node) { Type = ErrorType.FunctionParameterCountMismatch });
                    return null;
                }

                // Check that the parameter has the correct value
                Operator op = this.Parameters[0];
                ITypeDef optype = op.ComputeType(this.Event.EventType, false, report);
                if (optype == null)
                    return null;
                optype = this.ToInstance(ref op, optype, report);
                if (optype == null)
                    return null;
                this.Parameters[0] = op;
                if (!optype.CanBeAssignedTo(this.Event.EventType))
                {
                    report.Add(new ErrorEntry(this.Node) { Type = ErrorType.FunctionParameterTypeMismatch, Info = "0" });
                    return null;
                }

                // The return type is void
                return VoidType.Self;
            }

            // Is it a function object, i.e. some instance?
            if (type is InstanceType)
                type = (type as InstanceType).Type;
            // Is it a function set and we must find out which one to call?
            if (type is FunctionSetTypeDef)
            {
                FunctionTypeDef func = this.FindFunction(type as FunctionSetTypeDef, goal, report);
                // Nothing found? -> error
                if ( func == null )
                    return null;

                this.FunctionType = func;
                if ( this.Operator is NewOperator )                
                    (this.Operator as NewOperator).Constructor = func as Constructor;
                else if (this.Operator is AtomIdentifier)
                    (this.Operator as AtomIdentifier).Object = func;
                else if (this.Operator is PropertyOperator)
                    (this.Operator as PropertyOperator).Object = func as MemberFunction;

                return func.ReturnType;
            }

            // We can only invoke functions
            FunctionTypeDef ftype = type as FunctionTypeDef;
            if (ftype == null)
            {
                report.Add(new ErrorEntry(this.Node) { Type = ErrorType.NotAFunction });
                return null;
            }

            // Check the parameters
            if (!this.ComputeParameterType(ftype, report))
                return null;

            this.FunctionType = ftype;
            if (this.Operator is NewOperator)
                (this.Operator as NewOperator).Constructor = ftype as Constructor;
            //else if (this.Operator is AtomIdentifier)
            //    (this.Operator as AtomIdentifier).Object = ftype;
            //else if (this.Operator is PropertyOperator)
            //    (this.Operator as PropertyOperator).Object = ftype as MemberFunction;

            return ftype.ReturnType;
        }

        private bool ComputeParameterType(FunctionTypeDef funcTypeDef, ErrorReport report)
        {
            // Does the number of parameters match?
            if (this.Parameters.Count != funcTypeDef.ParameterTypes.Count)
            {
                report.Add(new ErrorEntry(this.Node) { Type = ErrorType.FunctionParameterCountMismatch });
                return false;
            }

            // Check each parameter
            for (var i = 0; i < this.Parameters.Count; ++i)
            {
                // Type according to the function signature
                ITypeDef t = funcTypeDef.ParameterTypes[i];
                // Type of the value passed to the function parameter
                Operator op = this.Parameters[i];
                ITypeDef optype = op.ComputeType(t, false, report);
                if (optype == null)
                    return false;
                optype = this.ToInstance(ref op, optype, report);
                if (optype == null)
                    return false;
                this.Parameters[i] = op;
                if (!optype.CanBeAssignedTo(t))
                {
                    report.Add(new ErrorEntry(this.Node) { Type = ErrorType.FunctionParameterTypeMismatch, Info = (i+1).ToString() });
                    return false;
                }
            }

            return true;
        }

        private FunctionTypeDef FindFunction(FunctionSetTypeDef settype, ITypeDef goal, ErrorReport report)
        {
            // Get a list of the parameters passed to the function
            List<ITypeDef> parameterTypes = new List<ITypeDef>();
            for (var i = 0; i < this.Parameters.Count; ++i)
            {
                // Type of the value passed to the function parameter
                Operator op = this.Parameters[i];
                ITypeDef optype = op.ComputeType(null, false, report);
                if (optype == null)
                    return null;
                parameterTypes.Add(optype);
            }

            FunctionTypeDef func = settype.FindFunction(parameterTypes, report, this.Node);
            return func;
        }
    }
}
